import mongoose, { Schema, Model, Document, HookNextFunction } from 'mongoose';
import validator from 'validator';
import jwt from 'jsonwebtoken';
import { UserPayload } from '../typings/jwt';

import bcrypt from 'bcryptjs';

// 编写models
// 写一个都能用到的用户名密码头像邮件的模型：
// 因为是实例上方法，所以UserDocument需要加属性。


// 这个schema本来里面传的泛型可以是任意值，
// 但是使用mongoose.model这个方法必须限定要继承Document:

// 因为是实例上方法，所以UserDocument需要加属性。
export interface IUserDocument extends Document {
	username: string,
	password: string,
	email: string;
	avatar: string;
	generateToken: () => string,
	// 就是_doc属性就是的也是IUserDocument类型。这个倒没什么太大用。
	_doc: IUserDocument
}

// 这个schema本来里面传的泛型可以是任意值，
// 但是使用mongoose.model这个方法必须限定要继承Document:
// 否则返回值没有Document上面的属性。



const UserSchema: Schema<IUserDocument> = new Schema({
	username: {
		type: String,
		required: [true, '用户名不能为空'],
		minlength: [6, '最小长度不能少于6位'],
		maxlength: [12, '最大长度不能大于12位']
	},
	password: String,
	avatar: String,
	email: {
		type: String,
		validate: { // valitator是一个验证的库。
			validator: validator.isEmail
		},
		trim: true,
	}
},

	// schema第二个参数可以让其自动添加创建时间和更新时间。
	// 由于不能把密码返回给前端，所以配置下toJson方法。转换给前端的数据格式

	// { timestamps: true }
	//transform转换返回的数据  _id变成id

	// 原来的数据返回是
	// d: "5ef0bc4e35ee1a81882e3a57"
	// username: "宝宝宝宝宝宝"
	// email: "baobao@126.com"
	// password: "$2a$10$O8p/DJuVL2PQcQMsbRlq6.hkNDl50OGyoNLvjSoQyXp6PzZLJDziS"
	// createdAt: "2020-06-22T14:12:30.747Z"
	// updatedAt: "2020-06-22T14:54:34.989Z"
	// __v: 0
	// avatar: "http://api.zhufeng.cn/uploads/1592837674987.JPG"

	//     // 由于不能把密码返回给前端，所以配置下toJson方法。
	//     // 因为前面加密时是拿用户id加密的，解密解出来也是个用户id，
	//     // 根据id去查用户信息，然后再返回筛选过的用户信息。
	{
		timestamps: true, toJSON: {
			transform: function (_doc: any, result: any) {
				console.log(_doc, '_doc', result, ';result');
				result.id = result._id;
				delete result._id;
				delete result.__v;
				delete result.password;
				delete result.createdAt;
				delete result.updatedAt;
				return result;
			},
		},
	}

);






const secret = process.env.JWT_SECRET_KEY! || 'zhufeng';

// 通过methods添加实例方法。
// 返回的jwt生成的token，jwt默认采用HMAC SHA256算法。
// token就完成了，客户端还需要带token过来进行验证。
// 做一个路由验证token。

// 从一个user 增加user实例的方法。。generateToken 为了从mongoose对象转成json对象
UserSchema.methods.generateToken = function (): string {
	let payload: UserPayload = ({ id: this._id });
	return jwt.sign(payload, secret, { expiresIn: '1h' });
}
// 一般在用户传来密码通过验证后就对其进行加密也可以，更好的方式是插入文档前进行：
// schema有个pre的方法可以完成这个工作，加密使用bcryptjs进行加密。

// hook钩子函数。每次保存一个文档的时候先走这个hook
UserSchema.pre<IUserDocument>('save', async function (next: HookNextFunction) {
	if (!this.isModified('password')) {
		return next();
	}
	try {
		this.password = await bcrypt.hash(this.password, 10);
		console.log(this.password, ' 保存的时候加密密码=》this.password')
		next();
	} catch (error) {
		next(error);
	}
});

// 这里把登录验证逻辑交给model了，所以需要给model扩展个方法。
// 在model里加入login的方法：
// 使用static可以增加model的方法，这个static在上面发的链接里有使用例子。

// 类似model 模型上增加静态方法。类似crete 。find等方法
UserSchema.static('login', async function (this: any, username: string, password: string): Promise<IUserDocument | null> {
	// 这个this指的是下面导出的User模型，因为要用model来查用户名，
	// 所以要使用this，ts可以在第一个参数声明this来防止报错。
	let user: IUserDocument | null = await this.model('User').findOne({ username });
	if (user) {
		const matched = await bcrypt.compare(password, user.password);
		if (matched) {
			return user;
		} else {
			return null;
		}
	}
	return user;
});


interface IUserModel<T extends Document> extends Model<T> {
	login: (username: string, password: string) => IUserDocument | null
}

// 使用static可以增加model的方法，这个static在上面发的链接里有使用例子。
// 这个this指的是下面导出的User模型，因为要用model来查用户名，所以要使用this，ts可以在第一个参数声明this来防止报错。
// 查到的user就是文档，然后通过bcryptjs比对。
// 由于我们添加了login的方法，所以导出的User类型对不上了，因为原本导出的类型是Model<UserDocument>，这个类型是没有login的方法的。所以我们需要对其扩展。
// 使用UserModel继承其原本类型，里面写上扩展的login方法。这样UserModel类型就是完整的我们要的model类型了。
// 但是下面导出不改任然会报错，还好mongoose的声明文件里用重载写了这种扩展情况：




export const User: IUserModel<IUserDocument> = mongoose.model<IUserDocument, IUserModel<IUserDocument>>('User', UserSchema);
